import math
from datetime import datetime
from itertools import groupby

from odoo import fields, models, api, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero, float_compare, float_round


class SSaleOrderMaterialPreparationWizard(models.Model):
    _name = 's.sale.order.material.preparation.wizard'
    _description = 'Sale Material Preparation'
    _rec_name = 'product_id'

    partner_id = fields.Many2one('res.partner', 'customer', readonly=True)
    sale_id = fields.Many2one('sale.order', 'Sales Order', readonly=True)
    sale_order_line_id = fields.Many2one('sale.order.line', 'Sales Order Line', readonly=True)
    # sale_order_line_ids_str = fields.Char('sale line str')
    # sale_order_line_ids = fields.One2many('sale.order.line', readonly=False, compute='_compute_sale_ids')

    forecast_order_id = fields.Many2one('s.forecast.order', string='Forecast Order')
    forecast_order_line_id = fields.Many2one('s.forecast.order.line', string='Forecast Order')

    product_id = fields.Many2one('product.product', 'Product')
    bom_id = fields.Many2one('mrp.bom')
    presets_bom_id = fields.Many2one('sb.presets.bom', 'presets bom')
    bom_type = fields.Selection([('bom', 'formal BOM'), ('presets', 'presets bom')], string='bom type', readonly=True)
    presets_bom_diff_id = fields.Many2one('sb.presets.bom.diff', 'presets bom diff')

    scrap_rate = fields.Float(string='Scrap Rate')

    prepare_qty = fields.Integer(string='Prepare Quantity', readonly=True)
    prepare_set_qty = fields.Integer(string='Prepare Quantity(SET)', readonly=True)
    prepare_pnl_qty = fields.Integer(string='Prepare Quantity(PNL)', readonly=True)
    prepare_sheet_qty = fields.Integer(string='Prepare Quantity(SHEET)', readonly=True)

    actual_planned_quantity = fields.Float('Actual planned quantity', readonly=True)
    planned_quantity = fields.Integer('Planned quantity')

    sheet_panel = fields.Integer(string='SHEET/PANEL', readonly=True)
    panel_pcs = fields.Integer(string='PANEL/PCS', readonly=True)
    panel_area = fields.Float(string='PANEL Area(m2)', readonly=True)
    panel_height = fields.Float(string='panel height', readonly=True)
    panel_width = fields.Float(string='panel width', readonly=True)
    sheet_height = fields.Float(string='sheet height', readonly=True)
    sheet_width = fields.Float(string='sheet width', readonly=True)
    pcs_total = fields.Integer(string='pcs total', readonly=True)
    uom_set_value = fields.Integer(string='SET', readonly=True)
    uom_pnl_value = fields.Integer(string='PNL', readonly=True)

    line_ids = fields.One2many('s.sale.order.material.preparation.line.wizard', 'order_id')

    prepared_materials = fields.Boolean(string='Prepared materials', default=False)

    preparation_type = fields.Char('preparation type')

    materials_delivery_date = fields.Date('materials delivery date')

    layers = fields.Integer('layers')

    product_lock_ids = fields.One2many('s.product.lock', 'preparation_id')
    product_lock_count = fields.Integer('product lock count', compute='_compute_product_lock_count', store=True)

    @api.depends('product_lock_ids')
    def _compute_product_lock_count(self):
        for rec in self:
            rec.product_lock_count = len(rec.product_lock_ids)

    @api.onchange('planned_quantity', 'scrap_rate')
    def onchange_qty(self):
        if self.bom_id:
            if self.uom_pnl_value == 0:
                self.panel_area = 0
            else:
                self.prepare_qty = math.ceil((self.planned_quantity * (1 + (self.scrap_rate / 100))) / (self.pcs_total)) * (self.pcs_total)
                self.prepare_sheet_qty = self.prepare_qty / self.pcs_total
                self.prepare_pnl_qty = self.prepare_sheet_qty * self.sheet_panel
                self.prepare_set_qty = self.prepare_sheet_qty * self.uom_set_value
                self.panel_area = self.sheet_height * self.sheet_width * self.prepare_sheet_qty / 1000000

            # 更新物料明细汇总
            for line in self.line_ids:
                if float_is_zero(self.scrap_rate, precision_rounding=0.01):
                    prepare_qty = 0
                else:
                    if line.product_id.categ_id.parent_id.code and line.product_id.categ_id.parent_id.code == 'CR':
                        prepare_qty = self.prepare_sheet_qty * line.base_qty

                    else:
                        prepare_qty = self.prepare_qty * line.base_qty
                transit_qty = line.product_id.s_contain_incoming_qty
                line.write({
                    'transit_qty': transit_qty,
                    # 'gap_qty': gap_qty,
                    'prepare_qty': prepare_qty,
                })

            # 更新拆分数据
            # for line in self.split_line_ids:
            #     line.qty = self.actual_planned_quantity * line.ratio
            #     line.panel_qty = line.qty / line.base_panel_qty

            # 更新物料清单行
            # self.bom_line_ids = [(5, 0, 0)]
            # bom_lines = self._get_bom_line_ids(self.bom_id, self.product_id, self.product_id.uom_id, self.actual_planned_quantity)
            # self.bom_line_ids = bom_lines
        elif self.presets_bom_id:
            if self.uom_pnl_value == 0:
                self.panel_area = 0
            else:
                self.prepare_qty = math.ceil((self.planned_quantity * (1 + (self.scrap_rate / 100))) / (self.pcs_total)) * (self.pcs_total)
                self.prepare_sheet_qty = self.prepare_qty / self.pcs_total
                self.prepare_pnl_qty = self.prepare_sheet_qty * self.sheet_panel
                self.prepare_set_qty = self.prepare_qty / self.uom_set_value
                self.panel_area = self.presets_bom_id.pnl_width * self.presets_bom_id.pnl_length * self.prepare_pnl_qty / 1000000

            for line in self.line_ids:
                presets_bom_line_id = line.presets_bom_line_id
                if float_is_zero(self.scrap_rate, precision_rounding=0.01):
                    prepare_qty = 0
                else:
                    # 芯板
                    if line.product_id.categ_id.parent_id.code and line.product_id.categ_id.parent_id.code == 'CR':
                        prepare_qty = math.ceil(self.prepare_sheet_qty * line.base_qty)
                    # 油墨，铜箔
                    elif line.product_id.categ_id.parent_id.code and line.product_id.categ_id.parent_id.code in ('IK'):
                        prepare_qty = presets_bom_line_id._compute_ink_qty() * self.prepare_pnl_qty
                    elif line.product_id.categ_id.parent_id.code and line.product_id.categ_id.parent_id.code in ('FO'):
                        prepare_qty = presets_bom_line_id._compute_ink_qty() * line.base_qty * self.prepare_pnl_qty
                    else:
                        prepare_qty = presets_bom_line_id.compute_product_qty(self.prepare_qty)
                transit_qty = line.product_id.s_contain_incoming_qty
                # prepare_qty = self.prepare_pnl_qty * line.base_qty
                # gap_qty = max([-(line.product_id.s_actual_quantity + line.product_id.s_contain_incoming_qty - line.product_id.s_lock_qty - prepare_qty), 0])
                line.write({
                    'transit_qty': transit_qty,
                    # 'gap_qty': gap_qty,
                    'prepare_qty': prepare_qty,
                })

        for li in self.line_ids:
            li.onchange_all_prepare()

    def _compute_scrap_rate(self, sale_order_line_id):
        product_lock_ids = self.env['s.product.lock'].search([
            ('sale_order_line_id', '=', sale_order_line_id.id),
            ('active', 'in', (True, False)),
            ('x_delete', '=', False),
            ('cancel', '=', True)
        ])
        if product_lock_ids:
            return max(product_lock_ids.mapped('scrap_rate')) * 100
        else:
            return 0

    @api.model
    def default_get(self, fields_list):
        res = super().default_get(fields_list)
        # active_ids = self._context.get('active_ids', [])
        preparation_type = self._context.get('preparation_type', '')
        active_model = self._context.get('active_model', '')
        default_sale_order_line_id = self._context.get('default_sale_order_line_id', '')
        sale_order_line_id = self.env['sale.order.line']
        default_forecast_order_line_id = self._context.get('default_forecast_order_line_id')
        forecast_order_line_id = self.env['s.forecast.order.line']

        if default_forecast_order_line_id:
            forecast_order_line_id = self.env['s.forecast.order.line'].browse(int(default_forecast_order_line_id))
            product_id = forecast_order_line_id.product_id
            res.update({
                'forecast_order_id': forecast_order_line_id.forecast_order_id.id,
                'partner_id': forecast_order_line_id.forecast_order_id.customer_id.id
            })
        elif default_sale_order_line_id:
            sale_order_line_id = self.env['sale.order.line'].browse(int(default_sale_order_line_id))
            product_id = sale_order_line_id.product_id
            res.update({
                'sale_id': sale_order_line_id.order_id.id,
                'partner_id': sale_order_line_id.order_id.partner_id.id
            })
        else:
            return res

        bom = self.env['mrp.bom']
        if product_id.bom_ids and active_model != 'sb.presets.bom.diff' and not product_id.s_disable_down_card:
            bom = product_id.bom_ids[0]
        presets_bom_id = self.env['sb.presets.bom'].search([('product_id', '=', product_id.id), ('sb_workflow_state', '=', 'approve')], limit=1)
        if not bom and not presets_bom_id:
            raise UserError(_('Product %s related BOM does not exist!', product_id.name))

        if preparation_type == 'supplement':
            if default_forecast_order_line_id:
                quantity = forecast_order_line_id.market_forecast_quantity
            else:
                quantity = sale_order_line_id.product_uom_qty
            res['preparation_type'] = preparation_type
            # 解锁补充带出报废率
            res['scrap_rate'] = self._compute_scrap_rate(sale_order_line_id)
        else:
            if forecast_order_line_id:
                quantity = max(forecast_order_line_id.market_forecast_quantity - forecast_order_line_id.pm_quantity, 1)
            else:
                quantity = max(sale_order_line_id.product_uom_qty - sale_order_line_id.s_pm_quantity, 1)

        # res.update({
        #     'sale_order_line': line.id,
        #     'sale_order_line_ids_str': ','.join([str(line.id) for line in line_ids]),
        #     'sale_order_line_ids': [(6, 0, line_ids.ids)],
        # })

        if product_id.detailed_type != 'product':
            return res

        # line_vals_list = []
        # 处理差异BOM
        # if active_model == 'sb.presets.bom.diff':
        #     diff = self.env['sb.presets.bom.diff'].search([('presets_bom_id', '=', presets_bom_id.id), ], limit=1)
        #     uom_pnl_value = diff.pnl_diff
        #     uom_set_value = diff.pcs_set_diff
        #     panel_quantity = math.ceil(quantity / uom_pnl_value)
        #     for line in presets_bom_id.bom_line_ids:
        #         value = line.product_qty
        #         line_vals_list.append((0, 0, {
        #             'product_id': line.product_id.id,
        #             'prepare_qty': panel_quantity * value,
        #             'transit_qty': line.product_id.s_contain_incoming_qty,
        #             'base_qty': value
        #         }))
        #     res.update({
        #         'product_id': product_id.id,
        #         'planned_quantity': quantity,
        #         'panel_pcs': uom_pnl_value,
        #         'uom_pnl_value': uom_pnl_value,
        #         'uom_set_value': uom_set_value,
        #         'presets_bom_id': presets_bom_id.id,
        #         'bom_type': 'presets',
        #         'presets_bom_diff_id': diff.id,
        #         'line_ids': line_vals_list,
        #         'scrap_rate': product_id.scrap_rate,
        #         'sheet_panel': 0,
        #         'sheet_width': 0,
        #         'sheet_height': 0,
        #     })
        #     return res

        if bom:
            res.update(self.get_bom_default(bom, product_id, quantity, preparation_type, forecast_order_line_id, sale_order_line_id))
        elif presets_bom_id:
            res.update(self.get_presets_default(presets_bom_id, product_id, quantity, preparation_type, forecast_order_line_id, sale_order_line_id))
        return res

    def get_bom_default(self, bom_id, product_id, quantity, preparation_type, forecast_order_line_id, sale_order_line_id):
        result = {}
        line_vals_list = []
        layers = product_id.param_line_ids.filtered(lambda x: x.attribute_id.name == '层数')
        domain = [('active', 'in', (True, False)), ('x_delete', '=', False), ('cancel', '=', True)]
        if forecast_order_line_id:
            domain.append(('forecast_order_line_id', '=', forecast_order_line_id.id))
        elif sale_order_line_id:
            domain.append(('sale_order_line_id', '=', sale_order_line_id.id))
        if product_id.makeup_product_ids:
            # makeup_setting_vals_list = []
            temporary_line_vals = {}
            sheet_qty = 0
            pcs_total = 0
            uom_set_value = 0
            sheet_height = 0
            sheet_width = 0
            panel_total_area = 0
            all_product = product_id.makeup_product_ids.mapped('product_id') + product_id
            all_pcs = 0
            for p in all_product:
                p_panel_height, p_panel_width, p_panel_qty, p_set_qty, p_sheet_qty, p_sheet_height, p_sheet_width = self._get_panel_width_height(p)
                all_pcs += p_sheet_qty * p_panel_qty

            for s_product in all_product:
                if not s_product.bom_ids:
                    raise UserError(_('The product %s is not configured with a BOM!', s_product.name))
                line_bom = s_product.bom_ids[0]
                line_panel_height, line_panel_width, line_panel_qty, line_set_qty, line_sheet_qty, line_sheet_height, line_sheet_width = self._get_panel_width_height(
                    s_product)
                ratio = line_sheet_qty * line_panel_qty / all_pcs
                sheet_qty += line_sheet_qty
                pcs_total += line_sheet_qty * line_panel_qty
                uom_set_value += line_sheet_qty * (line_panel_qty / line_set_qty)
                sheet_height = line_sheet_height
                sheet_width = line_sheet_width
                panel_total_area += (line_panel_height * line_panel_width * line_sheet_qty)

                # makeup_setting_vals_list.append((0, 0, {
                #     'product_id': s_product.id,
                #     'ratio': ratio,
                #     'base_panel_qty': line_panel_qty
                # }))
                for vals in self._get_line_vals_list(line_bom, quantity * ratio):
                    line_product_id = vals.pop('product_id')
                    if s_product in temporary_line_vals:
                        temporary_line_vals[line_product_id]['qty'] += vals['qty']
                        temporary_line_vals[line_product_id]['panel_qty'] += vals['panel_qty']
                        temporary_line_vals[line_product_id]['base_qty'] += vals['base_qty']
                    else:
                        temporary_line_vals[line_product_id] = vals
            uom_pnl_value = pcs_total / sheet_qty
            for key, value in temporary_line_vals.items():
                value.update({'product_id': key})
                line_vals_list.append((0, 0, value))

            # res['split_line_ids'] = makeup_setting_vals_list

        else:
            temporary_line_vals_list = self._get_line_vals_list(bom_id, quantity)
            product_lock_ids = self.env['s.product.lock'].search(domain)
            for vals in temporary_line_vals_list:
                vals_product_id = vals.get('product_id')
                if preparation_type == 'supplement':
                    replace_product_id = self.env['s.mrp.bom.product.replace'].search([
                        ('origin_product_id', '=', vals_product_id),
                        ('finished_product', '=', product_id.id)
                    ], limit=1)
                    if replace_product_id:
                        line_product_lock_ids = product_lock_ids.filtered(lambda x: x.product_id.id in (vals_product_id, replace_product_id.replace_product_id.id))
                    else:
                        line_product_lock_ids = product_lock_ids.filtered(lambda x: x.product_id.id == vals_product_id)

                    if not line_product_lock_ids:
                        continue
                    if vals_product_id != line_product_lock_ids[0].product_id.id:
                        vals['replace_product_id'] = line_product_lock_ids[0].product_id.id
                else:
                    replace_product_id = self._get_replace_product_id(vals_product_id, product_id, forecast_order_line_id, sale_order_line_id)
                    if replace_product_id:
                        vals['replace_product_id'] = replace_product_id.id
                line_vals_list.append((0, 0, vals))

            panel_height, panel_width, uom_pnl_value, uom_set_value, sheet_qty, sheet_height, sheet_width = self._get_panel_width_height(product_id)
            uom_set_value = sheet_qty * (uom_pnl_value / uom_set_value)

            pcs_total = uom_pnl_value * sheet_qty

        result.update({
            'product_id': product_id.id,
            'planned_quantity': quantity,
            'panel_pcs': bom_id.panel_pcs_ratio,
            'pcs_total': pcs_total,
            'uom_pnl_value': uom_pnl_value,
            'uom_set_value': uom_set_value,
            'bom_id': bom_id.id,
            'bom_type': 'bom',
            'line_ids': line_vals_list,
            'sheet_panel': sheet_qty,
            'sheet_width': sheet_width,
            'sheet_height': sheet_height,
            'layers': layers[0].value_id.name if layers else 0
        })
        return result

    def _get_replace_product_id(self, vals_product_id, product_id, forecast_order_line_id, sale_order_line_id):
        replace_product_id = self.env['s.mrp.bom.product.replace'].search([
            ('origin_product_id', '=', vals_product_id),
            ('finished_product', '=', self.product_id.id),
            '|',
            ('forecast_order_line_id', '=', forecast_order_line_id.id),
            ('sale_id', '=', sale_order_line_id.order_id.id),
        ], limit=1, order='id desc')
        if not replace_product_id:
            replace_product_id = self.env['s.mrp.bom.product.replace'].search([
                ('origin_product_id', '=', vals_product_id),
                ('finished_product', '=', product_id.id)
            ], limit=1, order='id desc')

        return replace_product_id.replace_product_id

    def get_presets_default(self, presets_bom_id, product_id, quantity, preparation_type, forecast_order_line_id, sale_order_line_id):
        if not presets_bom_id:
            return {}
        result = {}
        line_vals_list = []
        panel_sheet = presets_bom_id.pnl
        set_pnl = presets_bom_id.set_pnl
        pcs_set = presets_bom_id.pcs_set
        uom_pnl_value = set_pnl * pcs_set
        pcs_total = uom_pnl_value * panel_sheet  # 一个SHEET中的总PCS数
        uom_set_value = presets_bom_id.pcs_set
        for child in presets_bom_id.child_ids:
            uom_pnl_value += (child.set_pnl * child.pcs_set)
            pcs_total += (child.set_pnl * child.pcs_set * child.pnl)
        panel_quantity = math.ceil(quantity / uom_pnl_value)
        domain = [('active', 'in', (True, False)), ('cancel', '=', True)]
        if forecast_order_line_id:
            domain.append(('forecast_order_line_id', '=', forecast_order_line_id.id))
        elif sale_order_line_id:
            domain.append(('sale_order_line_id', '=', sale_order_line_id.id))
        product_lock_ids = self.env['s.product.lock'].search(domain)
        for line in presets_bom_id.bom_line_ids:
            vals = {}
            if preparation_type == 'supplement':
                replace_product_id = self.env['s.mrp.bom.product.replace'].search([
                    ('origin_product_id', '=', line.product_id.id),
                    ('finished_product', '=', product_id.id)
                ], limit=1)
                if replace_product_id:
                    line_product_lock_ids = product_lock_ids.filtered(
                        lambda x: x.product_id == replace_product_id.replace_product_id)
                else:
                    line_product_lock_ids = product_lock_ids.filtered(lambda x: x.product_id == line.product_id)

                if not line_product_lock_ids:
                    continue
                if line.product_id != line_product_lock_ids[0].product_id:
                    vals['replace_product_id'] = line_product_lock_ids[0].product_id.id

            value = line.product_qty
            vals.update({
                'product_id': line.product_id.id,
                'prepare_qty': panel_quantity * value,
                'transit_qty': line.product_id.s_contain_incoming_qty,
                'presets_bom_line_id': line.id,
                'base_qty': value
            })
            line_vals_list.append((0, 0, vals))

        result.update({
            'product_id': product_id.id,
            'planned_quantity': quantity,
            'sheet_panel': panel_sheet,
            'panel_pcs': uom_pnl_value,
            'pcs_total': pcs_total,
            'uom_pnl_value': uom_pnl_value,
            'uom_set_value': uom_set_value,
            'presets_bom_id': presets_bom_id.id,
            'bom_type': 'presets',
            'line_ids': line_vals_list,
            'sheet_width': 0,
            'sheet_height': 0,
            'layers': presets_bom_id.layer
        })
        return result

    def _get_bom_line_ids(self, bom, product_id, uom_id, qty):
        bom_lines = []

        for bom_line in bom.bom_line_ids:
            bom_lines.append((0, 0, {
                'product_id': bom_line.product_id.id,
                'qty': bom_line.product_qty * qty,
                'level': 1,
                'uom_id': bom_line.product_uom_id.id,
            }))
            # 递归处理子 BOM
            if bom_line.product_id.bom_ids:
                bom_lines += self._process_bom(bom_line.product_id.bom_ids[0], bom_line.product_qty * qty, 2)

        return bom_lines

    def _get_line_vals_list(self, bom, qty):
        line_vals_list = []
        material_vals = {}
        material_vals = self._get_bom_material_total(bom, material_vals)
        if material_vals:
            for product_id, value in material_vals.items():
                line_vals_list.append({
                    'product_id': product_id.id,
                    'prepare_qty': qty * value,
                    'transit_qty': product_id.s_contain_incoming_qty,
                    'base_qty': value
                })
        return line_vals_list

    def _get_bom_material_total(self, bom, vals):
        if not vals:
            vals = {}

        for line in bom.bom_line_ids:
            if line.child_bom_id:
                vals = self._get_bom_material_total(line.child_bom_id, vals)
            else:
                if line.product_id in vals:
                    # 芯板材料不根据物料清单中的用量计算
                    if line.product_id.categ_id.parent_id.code and line.product_id.categ_id.parent_id.code == 'CR':
                        vals[line.product_id] += 1
                    else:
                        vals[line.product_id] += line.product_qty
                else:
                    if line.product_id.categ_id.parent_id.code and line.product_id.categ_id.parent_id.code == 'CR':
                        vals[line.product_id] = 1
                    else:
                        vals[line.product_id] = line.product_qty
        return vals

    def _get_panel_width_height(self, product_id):
        uom_values = {x.uom_id.code: x.uom_value for x in product_id.uom_value_ids}
        panel_height = uom_values.get('PNLL', 0)
        panel_width = uom_values.get('PNLW', 0)
        panel_qty = uom_values.get('PANEL', 0)
        set_qty = uom_values.get('SET', 0)
        sheet_qty = uom_values.get('SHEET', 0)
        sheet_height = uom_values.get('SHETL', 0)
        sheet_width = uom_values.get('SHETW', 0)
        return panel_height, panel_width, panel_qty, set_qty, sheet_qty, sheet_height, sheet_width

    def _lock_stock(self):
        self.ensure_one()
        lock_stock_vals_list = []
        for line in self.line_ids:
            product_id = line.replace_product_id or line.product_id
            rounding = line.uom_id.rounding
            if float_is_zero(line.lock_stock_qty, precision_rounding=rounding):
                continue
            if float_compare(line.lock_stock_qty, line.free_stock_qty, precision_rounding=rounding) == 1:
                raise UserError(
                    _('product (%s), The locked quantity (%s) cannot exceed the inventory quantity (%s)',
                      product_id.name, str(line.lock_stock_qty), str(line.free_stock_qty)))
            lock_stock_vals = self._get_product_lock_vals()
            lock_stock_vals.update({
                'product_id': product_id.id,
                'qty': line.lock_stock_qty,
                'uom_id': line.uom_id.id,
                'lock_type': 'stock',
            })
            lock_stock_vals_list.append(lock_stock_vals)
        self.env['s.product.lock'].sudo().create(lock_stock_vals_list)

    def _lock_transit(self):
        self.ensure_one()
        lock_stock_vals_list = []
        for line in self.line_ids:
            product_id = line.replace_product_id or line.product_id
            rounding = line.uom_id.rounding
            if float_is_zero(line.lock_transit_qty, precision_rounding=rounding):
                continue
            if float_compare(line.lock_transit_qty, line.free_transit_qty, precision_rounding=rounding) == 1:
                raise UserError(
                    _('product (%s), The locked transit quantity (%s) cannot exceed the transit quantity (%s)', product_id.name, str(line.lock_transit_qty), str(line.free_transit_qty)))

            # 需要锁定的数量
            lock_transit_qty = line.lock_transit_qty

            # 查找在途的采购单明细
            purchase_line_ids = self.env['purchase.order.line'].search([
                ('product_id', '=', product_id.id),
                ('state', '!=', 'cancel'),
                ('s_close', '=', False)
            ])
            for p_line in purchase_line_ids:
                purchase_rounding = p_line.product_uom.rounding
                if float_compare(p_line.product_qty, p_line.qty_received, precision_rounding=purchase_rounding) != 1:
                    continue
                # qty = float_round(p_line.product_qty - p_line.qty_received, precision_rounding=purchase_rounding)
                qty = p_line.product_uom._compute_quantity(p_line.product_qty - p_line.qty_received, product_id.uom_id)
                p_lock_product_ids = self.env['s.product.lock'].search([
                    ('product_id', '=', product_id.id),
                    ('purchase_line_id', '=', p_line.id),
                    ('lock_type', '=', 'prepare')
                ])
                p_lock_qty = sum(p_lock_product_ids.mapped('qty'))
                qty = float_round(qty - p_lock_qty, precision_rounding=rounding)
                # 可锁定的数量为0，跳过
                if float_is_zero(qty, precision_rounding=rounding):
                    continue
                qty = min([qty, lock_transit_qty])
                lock_transit_qty = float_round(lock_transit_qty - qty, precision_rounding=rounding)
                lock_stock_vals = self._get_product_lock_vals()
                lock_stock_vals.update({
                    'product_id': product_id.id,
                    'qty': qty,
                    'uom_id': line.uom_id.id,
                    'lock_type': 'prepare',
                    'purchase_id': p_line.order_id.id,
                    'purchase_line_id': p_line.id,
                    'purchase_qty': p_line.product_qty,
                })
                lock_stock_vals_list.append(lock_stock_vals)
                if float_is_zero(lock_transit_qty, precision_rounding=rounding):
                    break

            # 请购
            if not float_is_zero(lock_transit_qty, precision_rounding=rounding):
                requisition_line = self.env['purchase.requisition.line'].search([
                    ('product_id', '=', product_id.product_tmpl_id.id),
                    ('requisition_id.state', 'in', ('section_purchasing', 'approved', 'countersignature'))
                ])
                for r_line in requisition_line:
                    r_rounding = r_line.product_uom_id.rounding
                    # 判断请购数量是否大于处理数，不大于跳过
                    if float_compare(r_line.product_qty, r_line.qty_received, precision_rounding=r_rounding) != 1:
                        continue

                    # 转换为库存单位数量
                    r_qty = r_line.product_uom_id._compute_quantity(r_line.product_qty - r_line.qty_received, product_id.uom_id)
                    r_lock_product_ids = self.env['s.product.lock'].search([
                        ('product_id', '=', product_id.id),
                        ('x_delete', '=', False),
                        ('purchase_requisition_id', '=', r_line.requisition_id.id)
                    ])
                    r_lock_qty = sum(r_lock_product_ids.mapped('qty'))

                    if float_compare(r_qty, r_lock_qty, precision_rounding=r_rounding) != 1:
                        continue

                    r_qty = float_round(r_qty - r_lock_qty, precision_rounding=r_rounding)
                    r_qty = min([r_qty, lock_transit_qty])
                    lock_transit_qty = float_round(lock_transit_qty - r_qty, precision_rounding=rounding)
                    r_lock_stock_vals = self._get_product_lock_vals()
                    r_lock_stock_vals.update({
                        'product_id': product_id.id,
                        'qty': r_qty,
                        'uom_id': line.uom_id.id,
                        'lock_type': 'prepare',
                        'purchase_requisition_id': r_line.requisition_id.id,
                    })
                    lock_stock_vals_list.append(r_lock_stock_vals)
                    if float_is_zero(lock_transit_qty, precision_rounding=rounding):
                        break
            if not float_is_zero(lock_transit_qty, precision_rounding=rounding):
                raise UserError(_('product (%s), Insufficient quantity locked in transit, please exit and prepare materials again!', product_id.name))
        self.env['s.product.lock'].sudo().create(lock_stock_vals_list)

    def _create_purchase_requisition(self):
        line_ids = self.line_ids.filtered(
            lambda x: float_compare(x.gap_qty, 0, precision_rounding=x.uom_id.rounding) == 1)

        requisition_ids = self.env['purchase.requisition']
        for category_id, g in groupby(line_ids.sorted(lambda x: x.category_id.parent_id.id), lambda x: x.category_id.parent_id):
            g_line_ids = self.env['s.sale.order.material.preparation.line.wizard'].concat(*g)

            # 请购明细
            line_vals_list = []
            # 锁定明细
            lock_prepare_vals_list = []
            for g_line in g_line_ids:
                requisition_qty = g_line.gap_qty
                g_product_id = g_line.replace_product_id or g_line.product_id
                if not g_product_id.purchase_ok:
                    return ValueError(_('Product % s has been prohibited from purchase!', g_product_id.name))
                qty = float_round(g_line.uom_id._compute_quantity(requisition_qty, g_product_id.uom_po_id), precision_rounding=0.1)
                if not qty:
                    qty = 0.1

                # 查找是否有审批中的请购
                requisition_line_ids = self.env['purchase.requisition.line'].search([
                    ('product_id', '=', g_product_id.product_tmpl_id.id),
                    ('requisition_id.state', 'in', ('wait_approval', 'draft')),
                    ('requisition_id.origin_sale_line_id', '!=', False)
                ])
                if requisition_line_ids:
                    for requisition_line in requisition_line_ids:
                        # 已锁定还未生效的
                        r_product_lock_ids = self.env['s.product.lock'].search([
                            ('purchase_requisition_line_id', '=', requisition_line.id),
                            ('x_delete', '=', False),
                            ('active', 'in', (False, True)),
                            ('lock_type', '=', 'prepare')
                        ])
                        stock_uom_qty = requisition_line.product_uom_id._compute_quantity(requisition_line.product_qty, g_product_id.uom_id)
                        # 请购多余空闲在途
                        r_available_qty = stock_uom_qty - sum(r_product_lock_ids.mapped('qty'))
                        # 多余的请购必须一次满足
                        if float_compare(requisition_qty, r_available_qty, precision_rounding=g_product_id.uom_id.rounding) != 1:
                            vals = self._get_product_lock_vals()
                            vals.update({
                                'product_id': g_product_id.id,
                                'qty': requisition_qty,
                                'uom_id': g_line.uom_id.id,
                                'lock_type': 'prepare',
                                'active': False,
                                'purchase_requisition_id': requisition_line.requisition_id.id,
                                'purchase_requisition_line_id': requisition_line.id,
                            })
                            self.env['s.product.lock'].create(vals)
                            requisition_qty = 0
                            break
                rounding = g_line.uom_id.rounding
                if not float_is_zero(requisition_qty, precision_rounding=rounding):

                    if not self.materials_delivery_date:
                        raise UserError(_('Please fill in the material delivery date!'))
                    line_vals_list.append((0, 0, {
                        'product_id': g_product_id.product_tmpl_id.id,
                        'product_qty': qty,
                        'product_uom_id': g_product_id.uom_po_id.id,
                        'requirement_date': self.materials_delivery_date
                    }))
                    lock_prepare_vals = self._get_product_lock_vals()
                    lock_prepare_vals.update({
                        'product_id': g_product_id.id,
                        'qty': requisition_qty,
                        'uom_id': g_line.uom_id.id,
                        'lock_type': 'prepare',
                        'active': False,
                    })
                    lock_prepare_vals_list.append(lock_prepare_vals)

            if line_vals_list:
                # 判断是否创建了
                requisition_id = self.env['purchase.requisition'].search([
                    ('presets_bom_diff_id', '=', self.presets_bom_diff_id.id)
                ])
                if requisition_id and self.presets_bom_diff_id:
                    pass
                else:
                    origin_list = [self.sale_id.name or '' + self.forecast_order_id.name or '']
                    requisition_id = self.env['purchase.requisition'].create({
                        'origin': ','.join(origin_list),
                        'origin_sale_line_id': self.sale_order_line_id.id,
                        'origin_sale_id': self.sale_order_line_id.order_id.id,
                        'pr_line_ids': line_vals_list,
                        'presets_bom_id': self.presets_bom_id.id,
                        'presets_bom_diff_id': self.presets_bom_diff_id.id,
                        'applicant': self.env.user.id,
                        'requisition_date': fields.Date.today(),
                        'product_categ_id': category_id.id,
                        'origin_partner_id': self.partner_id.id,
                        'origin_product_id': self.product_id.id,
                        'requisition_type': 'order'
                        # 'sb_workflow_template_id': 5,
                    })
                self.presets_bom_diff_id.state = 'generate'
                requisition_ids |= requisition_id

                product_lock_ids = self.env['s.product.lock'].create(lock_prepare_vals_list)

                for new_requisition_line in requisition_id.pr_line_ids:
                    product_lock_ids.filtered(lambda x: x.product_id.product_tmpl_id == new_requisition_line.product_id).write({
                        'purchase_requisition_id': requisition_id.id,
                        'purchase_requisition_line_id': new_requisition_line.id
                    })
        return requisition_ids

    def _get_product_lock_vals(self):
        self.ensure_one()
        return {
            'lock_date': datetime.now(),
            'sale_order_line_id': self.sale_order_line_id.id,
            'sale_id': self.sale_order_line_id.order_id.id,
            'forecast_order_id': self.forecast_order_id.id,
            'forecast_order_line_id': self.forecast_order_line_id.id,
            'origin_product_id': self.product_id.id,
            'scrap_rate': self.scrap_rate / 100,
            'prepare_qty': self.prepare_qty,
            'preparation_id': self.id,
            'preparation_type': 'bom' if self.bom_id else 'presets'
        }

    def _create_replace_product(self):
        replace_product_vals_list = []
        for s_line in self.line_ids.filtered(lambda x: x.replace_product_id):
            if s_line.product_id == s_line.replace_product_id:
                continue
            # 物料和替换物料分类和上级分类都不同，不允许替换
            if s_line.product_id.categ_id != s_line.replace_product_id.categ_id and s_line.product_id.categ_id.parent_id != s_line.replace_product_id.categ_id.parent_id:
                raise UserError(
                    _('Product %s, Replace Product %s, The classification of the two products is different and cannot be replaced！',
                      s_line.product_id.name, s_line.replace_product_id.name))
            replace_product_vals = {
                'finished_product': self.product_id.id,
                'origin_product_id': s_line.product_id.id,
                'replace_product_id': s_line.replace_product_id.id,
            }
            if not s_line.forever:
                replace_product_vals.update({
                    'sale_id': self.sale_order_line_id.order_id.id,
                    'forecast_order_id': self.forecast_order_id.id,
                    'forecast_order_line_id': self.forecast_order_line_id.id,
                })
            replace_product_vals_list.append(replace_product_vals)
        self.env['s.mrp.bom.product.replace'].sudo().create(replace_product_vals_list)

    def action_confirm(self):
        # 报废比例必须大于0
        if not self.scrap_rate:
            raise UserError(_('The scrap ratio must be greater than 0!'))
        # 不允许重复备料
        if self.prepared_materials and not self.presets_bom_diff_id:
            raise UserError(_('Duplicate material preparation is not allowed!'))

        self._create_replace_product()

        # 锁定库存物料
        self._lock_stock()

        # 锁在途
        self._lock_transit()

        # 请购
        requisition_ids = self._create_purchase_requisition()

        if requisition_ids:
            # 注释自动提交审批
            # for requisition in requisition_ids:
            #     requisition.create_workflow()
            action = self.env["ir.actions.act_window"]._for_xml_id('sb_workflow_purchase.purchase_requisition_approve_action')
            action['domain'] = [('id', 'in', requisition_ids.ids)]
        else:
            action = self.env["ir.actions.act_window"]._for_xml_id('sb_plan.action_s_material_preparation_sale_line_report')

        if self.preparation_type and self.preparation_type == 'supplement':
            product_lock_ids = self.env['s.product.lock'].search([
                ('sale_order_line_id', '=', self.sale_order_line_id.id),
                ('active', 'in', (True, False)),
                ('x_delete', '=', False),
                ('cancel', '=', True)
            ])
            product_lock_ids.write({'x_delete': True})
            product_lock_ids.sudo().action_archive()
        else:
            if self.sale_order_line_id:
                self.sale_order_line_id.write({'s_pm_quantity': self.prepare_qty + self.sale_order_line_id.s_pm_quantity})
            elif self.forecast_order_line_id:
                self.forecast_order_line_id.write({'pm_quantity': self.prepare_qty + self.forecast_order_line_id.pm_quantity})
                # if self.forecast_order_line_id.pm_quantity >= self.forecast_order_line_id.market_forecast_quantity:
                if self.forecast_order_line_id.forecast_order_id.state != 'done':
                    self.forecast_order_line_id.forecast_order_id.state = 'requisition_generated'
        # self.prepared_materials = True
        return action

    def action_view_product_lock_ids(self):
        self.ensure_one()
        action = self.env["ir.actions.act_window"]._for_xml_id('sb_base.action_s_product_lock')
        domain = [
            ('active', 'in', (True, False)),
            ('cancel', '=', False),
        ]
        if self.forecast_order_line_id:
            domain.append(('forecast_order_line_id', '=', self.forecast_order_line_id.id))
        elif self.sale_order_line_id:
            domain.append(('sale_order_line_id', '=', self.sale_order_line_id.id))
        action['domain'] = domain
        return action


class SSaleOrderMaterialPreparationLineWizard(models.Model):
    _name = 's.sale.order.material.preparation.line.wizard'
    _description = 'Sale Material Preparation Line'

    order_id = fields.Many2one('s.sale.order.material.preparation.wizard')
    product_id = fields.Many2one('product.product', 'Product')
    product_name = fields.Char('Product Name', related='product_id.name')
    default_code = fields.Char('Code', related='product_id.default_code')
    category_id = fields.Many2one('product.category', related='product_id.categ_id', string='Product Category')

    replace_product_id = fields.Many2one('product.product', 'Replace Product')

    base_qty = fields.Float('Base Quantity', digits=(10, 6))
    # qty = fields.Float('Quantity', digits='Product Unit of Measure')
    uom_id = fields.Many2one('uom.uom', string='Uom', related='product_id.uom_id')
    prepare_qty = fields.Float('prepare qty', digits='Product Unit of Measure')
    stock_qty = fields.Float('Stock Quantity', digits='Product Unit of Measure')
    transit_qty = fields.Float('transit qty', digits='Product Unit of Measure')

    free_stock_qty = fields.Float(
        'free stock quantity', digits='Product Unit of Measure', compute='_compute_free_stock_qty')
    free_transit_qty = fields.Float(
        'free transit quantity', digits='Product Unit of Measure', compute='_compute_free_stock_qty')
    lock_qty = fields.Float('locked qty', digits='Product Unit of Measure', compute='_compute_free_stock_qty', store=True)
    order_lock_qty = fields.Float('order locked qty', digits='Product Unit of Measure', compute='_compute_free_stock_qty', store=True)

    lock_stock_qty = fields.Float('lock stock qty', digits='Product Unit of Measure')
    lock_transit_qty = fields.Float('lock transit qty', digits='Product Unit of Measure')
    gap_qty = fields.Float('gap qty', digits='Product Unit of Measure')

    all_prepare = fields.Boolean(string='Pure procurement')
    presets_bom_line_id = fields.Many2one('sb.presets.bom.line', 'presets bom line')

    forever = fields.Boolean('forever', default=False)

    @api.depends('product_id', 'replace_product_id', 'product_id.s_lock_ids', 'replace_product_id.s_lock_ids')
    def _compute_free_stock_qty(self):
        for rec in self:
            product_id = rec.replace_product_id or rec.product_id
            rounding = product_id.uom_id.rounding
            free_stock_qty = float_round(
                product_id.qty_available - product_id.s_lock_stock_qty - product_id.outgoing_qty, precision_rounding=rounding)
            free_transit_qty = float_round(
                product_id.s_contain_incoming_qty - product_id.s_lock_prepare_qty, precision_rounding=rounding)

            free_stock_qty = max([free_stock_qty, 0])
            free_transit_qty = max([free_transit_qty, 0])
            lock_qty = product_id.s_lock_qty
            order_lock_qty = 0
            order_lock_ids = self.env['s.product.lock']
            if rec.order_id.sale_order_line_id:
                order_lock_ids |=  product_id.s_lock_ids.filtered(lambda x: x.sale_order_line_id == rec.order_id.sale_order_line_id)
            if rec.order_id.forecast_order_line_id:
                order_lock_ids |= product_id.s_lock_ids.filtered(lambda x: x.forecast_order_line_id == rec.order_id.forecast_order_line_id)
            if order_lock_ids:
                order_lock_qty = sum(order_lock_ids.mapped('qty'))
            rec.write({
                'free_stock_qty': free_stock_qty,
                'free_transit_qty': free_transit_qty,
                'lock_qty': lock_qty,
                'order_lock_qty': order_lock_qty,
            })

    @api.onchange('all_prepare', 'prepare_qty', 'replace_product_id')
    def onchange_all_prepare(self):
        self._compute_free_stock_qty()
        product_id = self.replace_product_id or self.product_id
        if not product_id:
            return

        self.write({
            'stock_qty': product_id.s_actual_quantity,
            'transit_qty': product_id.s_contain_incoming_qty,
            'lock_qty': product_id.s_lock_qty
        })

        # 实际需要备料的数量， 备料数量 - 已经锁定的数量
        actual_prepare_qty = self.prepare_qty - self.order_lock_qty
        if self.all_prepare:
            # gap_qty = max([-(self.transit_qty - self.prepare_qty), 0])
            self.write({
                'lock_stock_qty': 0,
                'lock_transit_qty': 0,
                'gap_qty': actual_prepare_qty
            })
        else:
            rounding = product_id.uom_id.rounding
            lock_stock_qty = min([actual_prepare_qty, self.free_stock_qty])
            actual_prepare_qty = float_round(actual_prepare_qty - lock_stock_qty, precision_rounding=rounding)
            lock_transit_qty = min([actual_prepare_qty, self.free_transit_qty])
            actual_prepare_qty = float_round(actual_prepare_qty - lock_transit_qty, precision_rounding=rounding)
            gap_qty = max([actual_prepare_qty, 0])

            if product_id.categ_id.parent_id.code and product_id.categ_id.parent_id.code == 'CR':
                lock_stock_qty = math.ceil(lock_stock_qty)
                lock_transit_qty = math.ceil(lock_transit_qty)
                gap_qty = math.ceil(gap_qty)

            self.write({
                'lock_stock_qty': lock_stock_qty,
                'lock_transit_qty': lock_transit_qty,
                'gap_qty': gap_qty,
                'uom_id': product_id.uom_id.id
            })
            # self.gap_qty = max([-(self.stock_qty + self.transit_qty - self.lock_qty - self.prepare_qty), 0])

    def action_view_lock_line(self):
        self.ensure_one()
        product_id = self.replace_product_id or self.product_id
        action = self.env["ir.actions.act_window"]._for_xml_id('sb_base.action_s_product_lock')
        action['domain'] = [('product_id', '=', product_id.id)]
        return action
